Python高级编程——描述符Descriptor超详细讲解(下篇之描述符三剑客)
送你小心心记得关注我哦!!
进入正文
本文声明:python的描述符descriptor,这是属于python高级编程的一些概念和实现方法,可能有很多的小伙伴还并没有用到过,但是在Python的面试过程中有可能会出现,究竟什么是python描述符,有什么作用,使用有什么意义,它的诞生背景是什么,很少有文章专门介绍这一块,有的文章介绍的太过粗浅,以至于看过之后依然不能够理解描述符的本质。上一篇文章中(中篇)已经讲到了关于属性的三中控制方式,讲解了三个魔术方法__getattr__、__setattr__、__delattr__的详细使用方法,也分析了它们的作用和不足之处。本文依然是分为上、中、下、补充篇四个系列部分进行讲解,本文是第三篇——下篇,介绍Python的描述符、描述符协议、描述符三剑客、描述符的详细实现等。
全文目录
01 到底什么是描述符——descriptor
1.1 什么是描述符——descriptor以及
相关的一系列定义
1.2 描述符的作用
1.3 描述符三个函数的定义形式
02 描述符的具体实现
2.1 认识描述符
2.2 类属性描述符
2.3 实例属性描述符
2.4 类属性描述符对象和实例属性同名时
03 属性优先访问级别总结
3.1 如果没有设置“描述符属性”
3.2 如果设置了“描述符属性”
3.3 疑惑不解
04 描述符的应用场景
05 描述符的底层应用(下期预告)
01
到底什么是描述符——descriptor
前面饶了很多弯子,一步一步引入属性访问的优先级顺序这样一个主题,然后是属性控制的三剑客,似乎还是和描述符descriptor没啥关系啊,关系自然是有的。本文会一一说明,首先我将一系列的概念和定义一次性的写出来,后面再加以分析说明。
(1)描述符:某个类,只要是内部定义了方法 __get__, __set__, __delete__ 中的一个或多个,就可以称为描述符,描述符的本质是一个类。
(2)描述符协议:描述符本质就是一个新式类,在这个新式类中,至少实现了__get__(),__set__(),__delete__()中的一个,这些魔术方法也被称为描述符协议
(3)非数据描述符:一个类,如果只定义了 __get__() 或者是__delete__()方法,而没有定义 __set__()方法,则认为是非数据描述符(即没有定义__set__)
(4)数据描述符:一个类,不仅定义了 __get__() 方法,还定义 __set__(), __delete__() 方法,则认为是数据描述符(即定义了__get__和__set__)
(5)描述符对象:描述符(即一个类,因为描述符的本质是类)的一个对象,一般是作为其他类对象的属性而存在
描述符就是一个“绑定行为“的对象属性,在描述符协议中,它可以通过方法充写属性的访问。我们或许经常看见描述符的作用描述中,有两个关键词“绑定行为”和“托管属性”,那到底是什么意思呢,我给出一个通俗的解释,
绑定行为:所谓的绑定行为,是指在属性的访问、赋值、删除时还绑定发生了其他的事情,正如前面属性控制三剑客所完成的事情一样;
托管属性:python描述符是一种创建“托管属性”的方法,即通过描述符(类)去托管另一个类的相关属性,也可以说是类的属性的一个代理。为了方便的理解“托管属性”这个概念,将通过几个通俗的例子去说明。
以人类而言,Person是一个类,人应该有很多属性,比如人是美丽的、性感的、成熟的、博学的、大方的等等,所谓的“描述”,本身指的就是描述某一个类的某一些特性的,在程序设计中,属性就是用来描述类的特征的,所谓的描述符(描述类)就是专门再创建一个类,让这个类去描述本身那个类的相关属性,这也正是“描述”二字的由来,其实和我们生活中的描述是一个意思。
描述符的作用是用来代理另外一个类的属性的
后面的代码也将从“绑定行为”和“托管属性”两个方面进行说明。
def __get__(self, instance, owner)
self:指的是描述符类的实例
instance:指的是使用描述符的那个类的实例,如
student。下面的instance一样的意思。
owner:指的是使用描述符的那个类,如Student
def __set__(self, instance, value)
def __delete__(self, instance,)
02
描述符的具体实现
前面讲了,要实现所谓的描述符,就是要实现上面的三个魔术方法,但是和普通类定义的方式不一样,因为“属性代理(属性托管)”的机制,我们需要定一两个类,一个类A,一个ADescriptor类,即所谓的描述类。
注意,不是直接在一个类中定义上面的描述符的三个方法哦!
#人的性格描述,悲观的?开朗的?敏感的?多疑的?活泼的?等等
class CharacterDescriptor:
def __init__(self, value):
self.value = value
def __get__(self, instance, owner):
print("访问性格属性")
return self.value
def __set__(self, instance, value):
print("设置性格属性值")
self.value = value
#人的体重描述,超重?过重?肥胖?微胖?合适?偏轻?太瘦?等等
class WeightDescriptor:
def __init__(self, value):
self.value = value
def __get__(self, instance, owner):
print("访问体重属性")
return self.value
def __set__(self, instance, value):
print("设置体重属性值")
self.value = value
class Person:
character=CharacterDescriptor('乐观的')
weight=WeightDescriptor(150)
p=Person()
print(p.character)
print(p.weight)
运行结果为:
访问性格属性
乐观的
访问体重属性
150
先不管运行结果,我们仅仅针对上面的代码,发现一个问题,现在明白为什么称描述符为“属性代理”了吧,他其实就是专门用一个类去装饰某一个属性,我可以把这个属性定义成任何我想要的样子,所谓的“一对一定制属性”。人有体重和性格这两个属性,当然我可以把这两个属性都定义在Person类里面,但是这就不方便为这个属性的操作绑定相应的行为,进行任意的个性化定制属性了,你也许会说,我依然可以通过“属性控制三剑客”完成啊,参见上一篇文章:
python高级编程——描述符Descriptor详解(中篇)——python对象的属性访问优先级与属性的控制与访问)
但是“属性控制三剑客”的缺点就是无法“一对一定制”,他虽然可以为属性绑定行为,但是任何属性都会绑定,不太方面将一个属性定制成任意我想要的样子。
再仔细一看,实际上完成了不就是Person的一个类属性本质上就是属性描述类的一个实例对象啊!哦,原来如此,的确如此,但是需要注意的是,在访问Person的这个类属性的时候,会发生一些特别的事情。因为我们发现,我们打印的print(p.character)中的character应该是CharacterDescriptor类的实例对象,为什么会打印出一个具体的值呢?这是因为:
访问Person的character属性时,调用了描述符CharacterDescriptor类的__get__()方法。这就达到了描述符的作用。
总结:对于类属性描述符,如果解析器发现属性property是一个描述符的话,它能把Class.x转换成Class.__dict__[‘property’].__get__(None, Class)来访问。
依然用上面的代码,只是下面添加以下几句话。
p=Person()
print(p.character) #属性的访问
print(p.weight) #
p.weight=200 #修改属性
print(p.weight)
del p.weight #删除属性
print(p.weight)
运行结果为:
访问性格属性
乐观的
访问体重属性
150
设置体重属性值
访问体重属性
200
删除体重属性
访问体重属性
Traceback (most recent call last):显示AttributeError: 'WeightDescriptor' object has no attribute 'value'
总结
(1)对于类装饰器属性,只要出现属性访问(不管是通过对象访问还是类名访问),都会优先调用装饰器的__get__方法;
(2)对于类装饰器属性,若出现属性修改(不管是通过对象访问还是类名访问),都会优先调用装饰器的__set__方法;
(3)对于类装饰器属性,若出现属性删除(不管是通过对象访问还是类名访问),都会优先调用装饰器的__delete__方法;
两个描述符类的代码不变,仅仅改变Person类的代码,如下:
class Person:
def __init__(self):
self.character=CharacterDescriptor('乐观的')
self.weight=WeightDescriptor(150)
p=Person()
print(p.character) #属性的访问
print(p.weight) #
p.weight=200 #修改属性
print(p.weight)
del p.weight #删除属性
print(p.weight)
运行结果为:
<__main__.CharacterDescriptor object at 0x000001963C643780>
<__main__.WeightDescriptor object at 0x000001963C6437B8>
200
Traceback (most recent call last):AttributeError: 'Person' object has no attribute 'weight'
为什么?
并没有像我们预期的那样调用__get__()、__set__()、__delete__()方法,只是说他是Descriptor的一个对象。
总结
描述符是一个类属性,必须定义在类的层次上, 而不能单纯的定义为对象属性。
通过上面的这几个例子,现在应该可以好好体会到“描述符”的两个层面的作用了:
绑定行为:在访问雷属性的时候,会打印出很多的额外信息,这不就是在添加额外的行为吗?
属性代理(托管属性):将某个属性专门用一个描述符(描述类)加以托管,实现任意的定制化,一对一的定制属性。
前面说了,描述符针对的是类属性,但是当一个类中,如果类属性是描述符对象,而实例属性由于这个描述符属性同名,这该怎么办呢?
class Person:
character=CharacterDescriptor('乐观的')
weight=WeightDescriptor(150)
def __init__(self,character,weight):
self.character=character
self.weight=weight
p=Person('悲观的',200)
print(p.character) #属性的访问
print(p.weight) #
运行结果为:
设置性格属性值
设置体重属性值
访问性格属性
悲观的
访问体重属性
200
从上面的运行结果可以看出,首先是访问了描述符的__set__方法,这是因为在构建对象的时候,相当于为character和weight赋值,然后再调用__get__方法,这是因为访问了类属性character和weight,但是最终打印出来值却并不是类属性的值,这是因为,实例属性实际上是在“描述符类属性”后面访问的,所以覆盖掉了。
总结
到目前为止,我们接触到的属性有很多了,实例属性,类属性、描述符类属性、父类的类属性、带有属性控制函数三剑客的属性等,那么当一个属性同名的时候,访问的优先级到底是什么样子呢?
03
属性的优先访问级别总结
没有设置描述符属性,则属性的优先访问顺序和我们前面文章里面所讲的是一样的,
(1) __getattribute__(), 无条件调用,任何时候都先调用
(2)实例属性
(3)类属性
(4)父类属性
(5) __getattr__() 方法 #如果所有的属性都没有搜索到,则才会调用该函数
注意:因为描述符属性本身就是定义在类里面的,也可以当成是类属性,但是它并不是一般的类属性,请记住一句话:
一旦一个属性被标记为“描述符属性”,那它的性质再也不会变,与它同名的变量不管是放在类
(1)先比较实例属性和描述符属性
class Person:
a2=CharacterDescriptor('乐观的')
def __init__(self):
self.a2='悲观的'
def __getattribute__(self,key):
print('__getattribute__')
return super(Person,self).__getattribute__(key)
def __getattr__(self,key):
print('__getattr__')
p=Person()
print(p.a2)
运行结果是:
设置性格属性值
__getattribute__
访问性格属性
悲观的
为什么会得到这样的结果?
第一句:设置性格属性值 :这是由p=Person()得到的,因为他会告诉你这是再给一个“描述符变量赋值,赋值为“悲观的”,所以调用了__set__”
后面三句:__getattribute__总是优先访问,而且访问的由于是“描述符变量”,故而访问的时候调用__get__
(2)类属性与描述符属性
class Person:
a2=CharacterDescriptor('乐观的')
a2='沮丧的'
def __init__(self):
pass
def __getattribute__(self,key):
print('__getattribute__')
return super(Person,self).__getattribute__(key)
def __getattr__(self,key):
print('__getattr__')
p=Person()
print(p.a2)
运行结果为:
__getattribute__
沮丧的
但是,这并不意味着类属性a2,就比描述符属性a2的优先级更高,仅仅是因为后面重新对a2进行复制,改变了a2的性质,不再是数据描述符,如果我交换两个a2的顺序,得到的结果为如下:
__getattribute__
访问性格属性
乐观的
因为此时,a2作为数据描述符存在。
我搜集了很多博文,看到很多博主得到了如下结论,导致我自己也没有得出一个确切的定论,所以希望再次与广大网友讨论,下面的两个结论都是从博客上摘录下来的。
(1)类属性 > 数据描述符 > 实例属性 > 非数据描述符 > 找不到的属性触发__getattr__()
这样的说法显然不严谨,因为类属性不总是优先于实例属性的
(2) __getattribute__()> 数据描述符> 实例对象的字典(若与描述符对象同名,会被覆盖哦)>类的字典>非数据描述符
>父类的字典>__getattr__() 方法
这样的说法也不严谨,因为从我上面的调试来看,当数据描述符属性与实力属性同名的时候,最终显示的值是实例属性的值,但是并不是实例属性覆盖了描述符属性,恰好相反,此时,实例属性也是当做描述属性那样去用的,而且调用了__get__和__set__方法。
总结
个人认为“描述符”的作用有其特殊性,它的目的并不是改变属性访问的优先级,根本目的只是改变属性的控制方式,方便对属性进行更好的访问、修改和删除,所以没必要死记硬背一个固定的优先级,在具体的问题中根据代码的运行能够做出合理的判断即可。当然如果哪一位小伙伴有更加权威的排序,也可以私下里告诉我哦,解答我心中的疑惑,将万分感谢!
04
“描述符”的应用场景
描述符的本质在于“描述”二字,最大的用处是对属性的个性定制与控制,如前所说,
(1)可以在设置属性时,做些检测等方面的处理
(2)设置属性不能被删除?那定义_delete_方法,并raise 异常。
(3)还可以设置只读属性
(4)把一个描述符作为某个对象的属性。这个属性要更改,比如增加判断,或变得更复杂的时候,所有的处理只要在描述符中操作就行了。
这一系列其实都是为了更好地去控制一个属性。
但是描述符因为它非常灵活的语法,可以实现一些非常高级的python特性,描述符是可以实现大部分python类特性中的底层魔法,含@classmethod,@staticmethd,@property甚至是__slots__属性,不仅如此,描述父是很多高级库和框架的重要工具之一,描述符通常是使用到装饰器或者元类的大型框架中的一个组件。
作为python使用者,可能绝大部分使用者对于描述符的一些高级设计不会涉及到,但是我们能够搞懂它的原理即可,关于描述符的这些高级应用,下面的一篇文章会继续讲解,有兴趣的小伙伴们可以继续关注一下!
推 荐 阅 读